
這篇文章會用 TDD 手刻 myFind、myFindLast、myIndexOfFirst、myIndexOfLast,再加上不帶 predicate 的 myIndexOf 和 myLastIndexOf
| Kotlin | C# LINQ | 備註 |
|---|---|---|
find { } |
FirstOrDefault(predicate) |
find 就是 firstOrNull 的別名 |
findLast { } |
LastOrDefault(predicate) |
|
indexOfFirst { } |
FindIndex() (List) |
C# 的 FindIndex 在 List<T> 上 |
indexOfLast { } |
FindLastIndex() (List) |
|
indexOf(element) |
IndexOf(element) |
用 == 比較 |
lastIndexOf(element) |
LastIndexOf(element) |
@Test
fun `find returns first matching element`() {
val numbers = listOf(1, 2, 3, 4, 5)
assertEquals(4, numbers.myFind { it > 3 })
}
@Test
fun `find returns null when no match`() {
val numbers = listOf(1, 2, 3)
assertNull(numbers.myFind { it > 10 })
}
@Test
fun `find first Engineering employee`() {
val result = employees.myFind { it.department == "Engineering" }
assertEquals("Alice", result?.name)
}
findLast 的測試對稱,資料照用,有命中的那兩個改成期望最後一個符合的元素
@Test
fun `findLast returns last matching element`() {
val numbers = listOf(1, 2, 3, 4, 5)
assertEquals(5, numbers.myFindLast { it > 3 })
}
@Test
fun `findLast returns null when no match`() {
val numbers = listOf(1, 2, 3)
assertNull(numbers.myFindLast { it > 10 })
}
@Test
fun `findLast last Engineering employee`() {
val result = employees.myFindLast { it.department == "Engineering" }
assertEquals("Grace", result?.name)
}
inline fun <T> Iterable<T>.myFind(predicate: (T) -> Boolean): T? {
return myFirstOrNull(predicate)
}
inline fun <T> Iterable<T>.myFindLast(predicate: (T) -> Boolean): T? {
return myLastOrNull(predicate)
}
沒錯,兩個都是一行搞定,直接委派給 day 06 手刻過的 myFirstOrNull 和 myLastOrNull
myFind 沒東西好重構,委派給 myFirstOrNull 本來就是 stdlib 的寫法。stdlib 的 find 長這樣
@kotlin.internal.InlineOnly
public inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
return firstOrNull(predicate)
}
findLast 則是這樣
@kotlin.internal.InlineOnly
public inline fun <T> Iterable<T>.findLast(predicate: (T) -> Boolean): T? {
return lastOrNull(predicate)
}
@kotlin.internal.InlineOnly
public inline fun <T> List<T>.findLast(predicate: (T) -> Boolean): T? {
return lastOrNull(predicate)
}
find 和 findLast 都是別名,一個對應 firstOrNull,一個對應 lastOrNull,都加了 @InlineOnly 註解。@InlineOnly 表示這個函式只在 inline 展開後的 bytecode 裡存在,不會產生實際的方法。list.find { it > 3 } 編譯後跟 list.firstOrNull { it > 3 } 完全一樣,連中間的函式呼叫都沒有
比較有意思的是 findLast 比 find 多了一個 List 的多載。find 不需要,firstOrNull 找到就停,正向走訪已經是最好的走法;lastOrNull 不一樣,day 06 提過它對 List 有從尾端往回走的快版本
問題是多載解析是靜態的。如果只宣告 Iterable<T>.findLast,就算呼叫端的接收者是 List,函式裡那句 lastOrNull(predicate) 看到的接收者型別還是 Iterable<T>,只會解析到全掃版。多宣告一個 List<T>.findLast,List 接收者才會優先選到它,反向走訪那個最佳化也才跟著吃得到
所以我們的 myFindLast 也要補上這個多載
inline fun <T> List<T>.myFindLast(predicate: (T) -> Boolean): T? {
return myLastOrNull(predicate)
}
day 06 已經幫 myLastOrNull 寫過 List 的反向版本,這裡照樣委派就好。後面 indexOfLast 的 List 多載是同一個脈絡
為什麼要有這兩個別名?語意。有些場景用 find 讀起來更自然:「在員工裡找出第一個工程師」比「取出第一個工程師或 null」口語化一些
@Test
fun `indexOfFirst returns index of first match`() {
val numbers = listOf(10, 20, 30, 40, 50)
assertEquals(3, numbers.myIndexOfFirst { it > 30 })
}
@Test
fun `indexOfFirst returns -1 when no match`() {
val numbers = listOf(1, 2, 3)
assertEquals(-1, numbers.myIndexOfFirst { it > 10 })
}
@Test
fun `indexOfFirst on empty list returns -1`() {
val empty = emptyList<Int>()
assertEquals(-1, empty.myIndexOfFirst { it > 0 })
}
@Test
fun `indexOfFirst Engineering employee`() {
assertEquals(0, employees.myIndexOfFirst { it.department == "Engineering" })
}
indexOfLast 一樣照著來,資料照用,有命中的那兩個改成期望最後一個符合的位置
@Test
fun `indexOfLast returns index of last match`() {
val numbers = listOf(10, 20, 30, 40, 50)
assertEquals(4, numbers.myIndexOfLast { it > 30 })
}
@Test
fun `indexOfLast returns -1 when no match`() {
val numbers = listOf(1, 2, 3)
assertEquals(-1, numbers.myIndexOfLast { it > 10 })
}
@Test
fun `indexOfLast on empty list returns -1`() {
val empty = emptyList<Int>()
assertEquals(-1, empty.myIndexOfLast { it > 0 })
}
@Test
fun `indexOfLast Engineering employee`() {
assertEquals(6, employees.myIndexOfLast { it.department == "Engineering" })
}
找不到回傳 -1 而不是 null。為什麼?跟 Java 的 List.indexOf() 慣例一致。Java 開發者已經習慣 -1 代表「沒找到」,Kotlin 選擇保持這個慣例來降低轉換成本
這個選擇看起來跟 day 06 講的 null safety 哲學有點矛盾,那邊講 Kotlin 寧可用 OrNull 也不靠 magic value。差別在於 Java 互通的代價,indexOf 是 kotlin.collections.List 的成員方法,在 JVM 上會對應到 java.util.List 的同名方法,如果改回傳 Int? 就會破壞跟 Java 程式碼的互通。這種地方 Kotlin 寧可保持一致也不追求理想,實用優先
inline fun <T> Iterable<T>.myIndexOfFirst(predicate: (T) -> Boolean): Int {
var index = 0
for (element in this) {
if (predicate(element)) {
return index
}
index++
}
return -1
}
跟 day 05 myFilterIndexed 一樣,手動維護 index 計數器。找到第一個符合的就馬上帶著 index 回去
myIndexOfLast 不能提前 return,得掃完
inline fun <T> Iterable<T>.myIndexOfLast(predicate: (T) -> Boolean): Int {
var lastIndex = -1
var index = 0
for (element in this) {
if (predicate(element)) {
lastIndex = index
}
index++
}
return lastIndex
}
lastIndex 初始化為 -1。每次遇到符合條件的元素就更新 lastIndex,走訪完再回傳。如果一個都沒找到,lastIndex 還是 -1
跟 day 06 myLast(predicate) 的模式一樣:myLast 記住最後一個符合的元素,myIndexOfLast 記住最後一個符合的位置
正向版本沒什麼好改的。不過 indexOfLast 其實有兩種走法:正向掃完、記住最後一個符合的位置(我們的寫法),或者從尾端反向走訪,碰到第一個符合的就提前回傳。Iterable 只能用前者,iterator 只會往前走;List 才能反向,所以跟 myFindLast 一樣補一個 List 的多載
inline fun <T> List<T>.myIndexOfLast(predicate: (T) -> Boolean): Int {
val iterator = listIterator(size)
while (iterator.hasPrevious()) {
if (predicate(iterator.previous())) {
return iterator.nextIndex()
}
}
return -1
}
listIterator(size) 拿到一個停在尾端的 iterator,hasPrevious() / previous() 一路往回走。previous() 之後游標會退一格,nextIndex() 剛好就是那個元素的位置。從後面找「最後一個」,碰到就直接回傳,不用掃完整個集合
@Test
fun `indexOf returns first occurrence`() {
val numbers = listOf(1, 2, 3, 2, 1)
assertEquals(1, numbers.myIndexOf(2))
}
@Test
fun `indexOf returns -1 when not found`() {
val numbers = listOf(1, 2, 3)
assertEquals(-1, numbers.myIndexOf(10))
}
@Test
fun `lastIndexOf returns last occurrence`() {
val numbers = listOf(1, 2, 3, 2, 1)
assertEquals(3, numbers.myLastIndexOf(2))
}
@Test
fun `lastIndexOf returns -1 when not found`() {
val numbers = listOf(1, 2, 3)
assertEquals(-1, numbers.myLastIndexOf(10))
}
重複元素是這組的重點。2 在 index 1 和 3 各出現一次,myIndexOf 要拿前面那個,myLastIndexOf 要拿後面那個
fun <T> Iterable<T>.myIndexOf(element: T): Int {
var index = 0
for (item in this) {
if (item == element) {
return index
}
index++
}
return -1
}
fun <T> Iterable<T>.myLastIndexOf(element: T): Int {
var lastIndex = -1
var index = 0
for (item in this) {
if (item == element) {
lastIndex = index
}
index++
}
return lastIndex
}
骨架跟 myIndexOfFirst / myIndexOfLast 完全一樣,只是把 predicate(item) 換成 item == element。myIndexOf(element) 跟 myIndexOfFirst { it == element } 是等價的,stdlib 兩個都留著,前者不需要 Lambda,用起來更簡潔
注意這兩個函式不需要 inline。沒接收 Lambda,就不會有 FunctionN 物件的問題
List 本身就有 indexOf 成員方法,接收者是 List 的時候,我們手動數的這一圈根本不用跑。stdlib 的做法是開頭先擋一下
fun <T> Iterable<T>.myIndexOf(element: T): Int {
if (this is List) {
return this.indexOf(element)
}
var index = 0
for (item in this) {
if (item == element) {
return index
}
index++
}
return -1
}
this is List 之後 this 會 smart cast 成 List<T>,this.indexOf(element) 呼叫到的就是成員方法。真正會走到下面手動計數的,只剩 Set 這種不是 List 的 Iterable
myLastIndexOf 同一招,開頭換成 if (this is List) return this.lastIndexOf(element)
原始碼位置:kotlin.collections 的 _Collections.kt
stdlib 的 Iterable<T>.indexOfFirst 跟我們幾乎一樣,只多一行
public inline fun <T> Iterable<T>.indexOfFirst(predicate: (T) -> Boolean): Int {
var index = 0
for (item in this) {
checkIndexOverflow(index)
if (predicate(item))
return index
index++
}
return -1
}
多出來的 checkIndexOverflow(index) 是在防溢位:Iterable 沒有大小上限,元素數量一旦超過 Int.MAX_VALUE,index++ 會繞回負數,stdlib 選擇在這時直接丟例外,而不是默默回傳一個錯的位置。另外還有一個定義在 List<T> 上的多載,List 的 size 本身就是 Int,不可能溢位,所以沒有這個檢查,那個版本才是跟我們寫的一模一樣
List<T>.indexOfLast 就是剛剛 Refactor 那個反向走訪,stdlib 也是這樣寫
public inline fun <T> List<T>.indexOfLast(predicate: (T) -> Boolean): Int {
val iterator = this.listIterator(size)
while (iterator.hasPrevious()) {
if (predicate(iterator.previous())) {
return iterator.nextIndex()
}
}
return -1
}
indexOf 在 stdlib 裡有兩層。kotlin.collections.List 介面本身就宣告了 indexOf 成員方法(JVM 上對應到 java.util.List,但這是 common 宣告,Native 和 JS 上同樣有);另一層是 Iterable<T>.indexOf(element) 擴充函式,開頭那個 if (this is List) 短路就是我們剛剛 Refactor 抄過來的,不是 List 才手動計數
find 是 firstOrNull 的語意包裝,indexOfFirst / indexOfLast 是帶 predicate 的位置尋找,indexOf / lastIndexOf 是用 == 的位置尋找。模式都在前幾篇看過了,這篇算是尋找系列的最後幾塊拼圖
下一篇是篩選與尋找篇的收尾:contains 和 elementAt
同步刊登於 Blog
圖片來源:AI 產生